// Copyright 2020 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.share.long_screenshots.bitmap_generation;

import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.Size;

import androidx.annotation.VisibleForTesting;

import org.chromium.base.ObserverList;
import org.chromium.chrome.browser.share.long_screenshots.LongScreenshotsMetrics;
import org.chromium.chrome.browser.share.long_screenshots.bitmap_generation.LongScreenshotsEntry.EntryStatus;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.components.paintpreview.player.CompositorStatus;

import java.util.ArrayList;
import java.util.List;

/**
 * Entry manager responsible for managing all the of the {@LongScreenshotEntry}. This should be used
 * to generate and retrieve the needed bitmaps. The first bitmap can be generated by calling
 * {@link generateInitialEntry}.
 */
public class EntryManager {
    private static final int KB_IN_BYTES = 1024;
    // List of all entries in correspondence of the webpage.
    private List<LongScreenshotsEntry> mEntries;
    // List of entries that are queued to generate the bitmap. Entries should only be queued
    // while the capture is in progress.
    private List<LongScreenshotsEntry> mQueuedEntries;
    private BitmapGenerator mGenerator;
    private ObserverList<BitmapGeneratorObserver> mGeneratorObservers;
    private @EntryStatus int mGeneratorStatus;
    private ScreenshotBoundsManager mBoundsManager;
    private int mMemoryUsedInKb;
    private int mMaxMemoryUsageInKb;
    private static final String TAG = "long_screenshot";

    /**
     * Users of the {@link EntryManager} can implement this interface to be notified of changes to
     * the generator.
     */
    public interface BitmapGeneratorObserver {
        /**
         * Called when the generator status changes.
         * @param status current status.
         */
        void onStatusChange(@EntryStatus int status);

        /**
         * Called when the compositor is ready.
         * @param contentSize size of the main frame.
         * @param scrollOffset the offset of the viewport rect relative to the main frame.
         */
        void onCompositorReady(Size contentSize, Point scrollOffset);
    }

    /**
     * @param context An instance of current Android {@link Context}.
     * @param tab Tab to generate the bitmap for.
     * @param inMemory Use memory buffers to store the capture rather than temporary files.
     */
    public EntryManager(Context context, Tab tab, boolean inMemory) {
        this(new ScreenshotBoundsManager(context, tab), tab, inMemory);
    }

    /**
     * @param boundsManager A {@link ScreenshotBoundsManager}.
     * @param tab Tab to generate the bitmap for.
     * @param inMemory Use memory buffers to store the capture rather than temporary files.
     */
    public EntryManager(ScreenshotBoundsManager boundsManager, Tab tab, boolean inMemory) {
        mEntries = new ArrayList<LongScreenshotsEntry>();
        mQueuedEntries = new ArrayList<LongScreenshotsEntry>();
        mGeneratorObservers = new ObserverList<>();
        mBoundsManager = boundsManager;

        mGenerator = new BitmapGenerator(tab, mBoundsManager, createBitmapGeneratorCallback());
        mGenerator.captureTab(inMemory);
        updateGeneratorStatus(EntryStatus.CAPTURE_IN_PROGRESS);
        // TODO(cb/1153969): Remove, or make this a finch param. Consider increasing default.
        mMaxMemoryUsageInKb = 16 * 1024;
    }

    @VisibleForTesting
    public BitmapGenerator getBitmapGeneratorForTesting() {
        return mGenerator;
    }

    /**
     * Generates the first bitmap of the page that is the height of the phone display. Callers of
     * this function should add a listener to the returned entry to get that status of the
     * generation and retrieve the bitmap.
     */
    public LongScreenshotsEntry generateInitialEntry() {
        LongScreenshotsEntry entry = new LongScreenshotsEntry(
                mGenerator, mBoundsManager.getFullEntryBounds(), this::updateMemoryUsage);
        processEntry(entry, false, false);
        // Pre-compute these entries so that they are ready to go when the user starts scrolling.
        // Not used when requesting a full page.
        // TODO(skare): We're currently always requesting a full page. Fully clean up if testing
        // full-page bitmap has acceptable memory usage.
        // getPreviousEntry(entry.getId());
        // getNextEntry(entry.getId());
        return entry;
    }

    /**
     * Creates the entry to generate the bitmap before the one passed in.
     *
     * @param relativeToId Id to base the new entry off of.
     * @return The new entry that generates the bitmap.
     */
    public LongScreenshotsEntry getPreviousEntry(int relativeToId) {
        int found = -1;
        for (int i = 0; i < mEntries.size(); i++) {
            if (mEntries.get(i).getId() == relativeToId) {
                found = i;
            }
        }

        if (found == -1) {
            return null;
        }

        if (found > 0) {
            return mEntries.get(found - 1);
        }

        // Before generating a new bitmap, make sure too much memory has not already been used.
        if (mMemoryUsedInKb >= mMaxMemoryUsageInKb) {
            return LongScreenshotsEntry.createEntryWithStatus(EntryStatus.INSUFFICIENT_MEMORY);
        }

        Rect bounds = mBoundsManager.calculateClipBoundsAbove(mEntries.get(0).getId());
        if (bounds == null) {
            return LongScreenshotsEntry.createEntryWithStatus(EntryStatus.BOUNDS_ABOVE_CAPTURE);
        }

        // found = 0
        LongScreenshotsEntry newEntry =
                new LongScreenshotsEntry(mGenerator, bounds, this::updateMemoryUsage);
        processEntry(newEntry, false, true);
        return newEntry;
    }

    /**
     * Creates the entry to generate the bitmap after the one passed in.
     *
     * @param relativeToId Id to base the new entry off of.
     * @return The new entry that generates the bitmap.
     */
    public LongScreenshotsEntry getNextEntry(int relativeToId) {
        int found = -1;
        for (int i = 0; i < mEntries.size(); i++) {
            if (mEntries.get(i).getId() == relativeToId) {
                found = i;
            }
        }

        if (found == -1) {
            return null;
        }

        if (found < mEntries.size() - 1) {
            return mEntries.get(found + 1);
        }

        // Before generating a new bitmap, make sure too much memory has not already been used.
        if (mMemoryUsedInKb >= mMaxMemoryUsageInKb) {
            return LongScreenshotsEntry.createEntryWithStatus(EntryStatus.INSUFFICIENT_MEMORY);
        }

        // found = last entry in the arraylist
        int newStartY = mEntries.get(mEntries.size() - 1).getEndYAxis() + 1;

        Rect bounds = mBoundsManager.calculateClipBoundsBelow(newStartY);
        if (bounds == null) {
            return LongScreenshotsEntry.createEntryWithStatus(EntryStatus.BOUNDS_BELOW_CAPTURE);
        }

        LongScreenshotsEntry newEntry =
                new LongScreenshotsEntry(mGenerator, bounds, this::updateMemoryUsage);
        processEntry(newEntry, false, false);
        return newEntry;
    }

    /**
     * Generates the bitmap of content within the bounds passed.
     * @param bounds bounds to generate the bitmap from.
     * @param updateMemoryUsage The callback to be notified of the bitmap memory usage.
     * @return The new entry that generates the bitmap.
     */
    public LongScreenshotsEntry generateEntry(Rect bounds) {
        LongScreenshotsEntry entry = new LongScreenshotsEntry(mGenerator, bounds, (bytes) -> {});
        processEntry(entry, true, false);
        return entry;
    }

    private void processEntry(LongScreenshotsEntry entry, boolean skipAddingEntryToList,
            boolean addToBeginningOfList) {
        if (mGeneratorStatus == EntryStatus.CAPTURE_COMPLETE) {
            entry.generateBitmap();
        } else if (mGeneratorStatus == EntryStatus.CAPTURE_IN_PROGRESS) {
            mQueuedEntries.add(entry);
        } else {
            entry.updateStatus(mGeneratorStatus);
        }

        if (skipAddingEntryToList) return;

        // Add to the list of all entries
        if (addToBeginningOfList) {
            mEntries.add(0, entry);
        } else {
            mEntries.add(entry);
        }
    }

    /**
     * Updates based on the generator status. If the capture is complete, generates the bitmap for
     * all the queued entries.
     *
     * @param status New status from the generator.
     */
    private void updateGeneratorStatus(@EntryStatus int status) {
        mGeneratorStatus = status;

        if (status == EntryStatus.CAPTURE_COMPLETE) {
            for (LongScreenshotsEntry entry : mQueuedEntries) {
                entry.generateBitmap();
            }
            mQueuedEntries.clear();
        } else {
            for (LongScreenshotsEntry entry : mQueuedEntries) {
                entry.updateStatus(status);
            }
        }

        for (BitmapGeneratorObserver observer : mGeneratorObservers) {
            observer.onStatusChange(mGeneratorStatus);
        }
    }

    private void updateMemoryUsage(int bytedUsed) {
        mMemoryUsedInKb += (bytedUsed / KB_IN_BYTES);
    }

    public void addBitmapGeneratorObserver(BitmapGeneratorObserver observer) {
        mGeneratorObservers.addObserver(observer);

        observer.onStatusChange(mGeneratorStatus);
        if (mGeneratorStatus == EntryStatus.CAPTURE_COMPLETE) {
            observer.onCompositorReady(mGenerator.getContentSize(), mGenerator.getScrollOffset());
        }
    }

    public void removeBitmapGeneratorObserver(BitmapGeneratorObserver observer) {
        mGeneratorObservers.removeObserver(observer);
    }

    /**
     * Creates the default BitmapGenerator to be used to retrieve the state of the generation. This
     * is the default implementation and should only be overridden for tests.
     */
    @VisibleForTesting
    public BitmapGenerator.GeneratorCallBack createBitmapGeneratorCallback() {
        return new BitmapGenerator.GeneratorCallBack() {
            @Override
            public void onCompositorResult(@CompositorStatus int status) {
                if (status == CompositorStatus.STOPPED_DUE_TO_MEMORY_PRESSURE
                        || status == CompositorStatus.SKIPPED_DUE_TO_MEMORY_PRESSURE) {
                    updateGeneratorStatus(EntryStatus.INSUFFICIENT_MEMORY);
                    LongScreenshotsMetrics.logLongScreenshotsEvent(
                            LongScreenshotsMetrics.LongScreenshotsEvent
                                    .GENERATOR_COMPOSITOR_MEMORY_PRESSURE);
                } else if (status == CompositorStatus.OK) {
                    updateGeneratorStatus(EntryStatus.CAPTURE_COMPLETE);
                    LongScreenshotsMetrics.logLongScreenshotsEvent(
                            LongScreenshotsMetrics.LongScreenshotsEvent
                                    .GENERATOR_COMPOSITOR_CAPTURE_COMPLETE);

                    Size contentSize = mGenerator.getContentSize();
                    Point scrollOffset = mGenerator.getScrollOffset();
                    for (BitmapGeneratorObserver observer : mGeneratorObservers) {
                        observer.onCompositorReady(contentSize, scrollOffset);
                    }
                } else {
                    updateGeneratorStatus(EntryStatus.GENERATION_ERROR);
                    LongScreenshotsMetrics.logLongScreenshotsEvent(
                            LongScreenshotsMetrics.LongScreenshotsEvent
                                    .GENERATOR_COMPOSITOR_GENERATION_ERROR);
                }
            }

            @Override
            public void onCaptureResult(@Status int status) {
                if (status == Status.LOW_MEMORY_DETECTED) {
                    updateGeneratorStatus(EntryStatus.INSUFFICIENT_MEMORY);
                    LongScreenshotsMetrics.logLongScreenshotsEvent(
                            LongScreenshotsMetrics.LongScreenshotsEvent
                                    .GENERATOR_CAPTURE_INSUFFICIENT_MEMORY);
                } else if (status != Status.OK) {
                    updateGeneratorStatus(EntryStatus.GENERATION_ERROR);
                    LongScreenshotsMetrics.logLongScreenshotsEvent(
                            LongScreenshotsMetrics.LongScreenshotsEvent
                                    .GENERATOR_CAPTURE_GENERATION_ERROR);
                }
            }
        };
    }

    public void destroy() {
        if (mGenerator != null) {
            mGenerator.destroy();
            mGenerator = null;
        }
    }
}
